﻿#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import logging
import os
import sys
import yaml

from oriolusgaffer.gaffer.catalogue import Catalogue
from oriolusgaffer.gaffer.selectstrategy import LatestStrategy
from oriolusgaffer.cache import Cache
from oriolusdocker.docker import DockerError, DockerFactory, DockerRemoveError
from orioluspith.packets.configreader import ConfigReader
from orioluspith.packets.metareader import FileMetaReader
from orioluspith.packets.packet import FilePacket, FullName, Status
from oriolustrotter.trotter import TrotterFactory

class GafferError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)
        
class GafferFactoryError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)

def __logerror__(logger, message):
    if logger.isEnabledFor(logging.DEBUG):
        logger.exception(message)
    else:
        logger.error(message)
        
class Gaffer:
    def __init__(self, cache, docker, trotter, catalogue, select_strategy):
        self.__cache = cache
        self.__docker = docker
        self.__trotter = trotter
        self.__catalogue = catalogue
        self.__select_strategy = select_strategy
        self.__logger = logging.getLogger("Gaffer")

    def update_list(self):
        self.__trotter.update()

    def install(self, packet_names, virtual_docker=None):
        self.__logger.info("Packet names were passed to install: %s" % str(packet_names))
        fitted_full_names = self.__catalogue.identify_packets(packet_names)
        selected_full_names = self.__select_strategy.select_packets(fitted_full_names)
        self.__logger.info("Packet names will be installed: %s" % map(str, selected_full_names))

        if virtual_docker is None:
            virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        full_names_to_replace = self.__find_packets_to_replace(selected_full_names, self.__docker)

        try:
            ordered_replaces = self.__remove_packets(full_names_to_replace, virtual_docker)
            self.__install_packets(selected_full_names, virtual_docker)
        except DockerError, error:
            message = "Installation of the packets %s can not be done! %s" % (map(str, selected_full_names), error)
            __logerror__(self.__logger,message) 
            raise GafferError(message)
        else:
            try:
                self.__remove_packets(ordered_replaces, self.__docker)
                self.__install_packets(selected_full_names, self.__docker)
            except DockerError, error:
                message = "An error has been occurred during installation of the packets %s! " \
                                  "Error description: %s" % (map(str, selected_full_names), error)
                __logerror__(self.__logger,message) 
                raise GafferError(message)

    def remove(self, packet_names):
        self.__logger.info("Packet names were passed to remove: %s" % str(packet_names))
        full_names_to_remove = map(FullName.parse, packet_names)

        virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        try:
            ordered_full_names_to_remove = self.__remove_packets(full_names_to_remove, virtual_docker)
        except DockerError, error:
            message = "Remove of the packets [%s] can not be done! %s" % (", ".join(packet_names), error)
            __logerror__(self.__logger,message) 
            raise GafferError(message)
        else:
            try:
                self.__remove_packets(ordered_full_names_to_remove, self.__docker)
            except DockerError, error:
                message = "An error has been occurred during remove of the packets [%s]! " \
                                  "Error description: %s" % (", ".join(packet_names), error)
                __logerror__(self.__logger,message) 
                raise GafferError(message)

    def upgrade(self):
        installed_packets = self.__docker.search(get_all=True)
        installed_packets_full_names = [packet.name for packet in installed_packets]
        listed_packets = self.__trotter.get_packet_list()
        self.__logger.debug("Installed packets before upgrade: %s" % str(installed_packets_full_names))
        self.__logger.debug("Available packets before upgrade: %s" % str(listed_packets))
                       
        full_names = map(FullName.parse, installed_packets_full_names + listed_packets)        
        selected_full_names = self.__select_strategy.select_packets(full_names) 

        full_names_to_remove = []
        full_names_to_install = []

        for selected_full_name in selected_full_names:
            name = selected_full_name.name
            installed = [FullName.parse(full_name) for full_name in installed_packets_full_names if FullName.parse(full_name).name == name]
            if len(installed) == 0:
                continue 

            if str(selected_full_name) in listed_packets:
                if selected_full_name in installed:      
                    to_remove = [full_name for full_name in installed if full_name != selected_full_name]
                    full_names_to_remove.extend(to_remove)
                else:
                    full_names_to_remove.extend(installed)
                    full_names_to_install.append(selected_full_name)
            else:
                self.__logger.warning("Packet '%s' is installed but omitted on servers" % str(selected_full_name))

        virtual_docker = DockerFactory().get_virtual_docker(self.__docker)

        full_names_to_replace = self.__find_packets_to_replace(full_names_to_install, self.__docker)
        for full_name in full_names_to_replace:
            if full_name in full_names_to_remove:
                full_names_to_remove.append(full_name)

        self.__logger.info("Packets will be installed: %s" % map(str, full_names_to_install))
        self.__logger.info("Packets will be removed: %s" % map(str, full_names_to_remove))
        try:
            ordered_removes = self.__remove_packets(full_names_to_remove, virtual_docker)
            self.__install_packets(full_names_to_install, virtual_docker)
        except DockerError, error:
            message = "Installation of the packets %s can not be done! %s" % (map(str, full_names_to_install), error)
            __logerror__(self.__logger,message)
            raise GafferError(message)
        else:
            try:
                self.__remove_packets(ordered_removes, self.__docker)
                self.__install_packets(full_names_to_install, self.__docker)
            except DockerError, error:
                message = "An error has been occurred during installation of the packets %s! " \
                                  "Error description: %s" % (map(str, full_names_to_install), error)
                __logerror__(self.__logger,message)
                raise GafferError(message)

    def __find_packets_to_replace(self, packet_full_names, docker):
        replaces_full_names = []
        for full_name_object in packet_full_names:
            self.__find_replaces(docker, full_name_object, replaces_full_names)

        return replaces_full_names

    def __find_replaces(self, docker, full_name_object, replaces_full_names):
        if not self.__is_packet_installed(docker, str(full_name_object)):
            path = self.__locate_package(full_name_object)                
            self.__find_installed_replaces(docker, path, replaces_full_names)

    def __find_installed_replaces(self, docker, path, replaces_full_names):
         packet = FilePacket(path)
         meta = packet.meta

         for replace in meta.replaces:
            name = replace["Name"]
            all_packets = docker.search(name=name, status=Status.INSTALLED)
            packet_full_names_in_replaces = self.__get_versions_in_replaces(meta, all_packets)

            for full_name in packet_full_names_in_replaces:
                if not full_name in replaces_full_names:
                    replaces_full_names.append(full_name)

         for dependency in meta.depends:
            packet_fullname = self.__select_dependent_packet_full_name(dependency, meta)
            self.__find_replaces(docker, packet_fullname, replaces_full_names)

    def __get_versions_in_replaces(self, meta, packets):
        return [FullName(packet.meta.name, packet.meta.version) for packet in packets if meta.is_in_replaces(packet.meta.name, packet.meta.version)]

    def __select_dependent_packet_full_name(self, dependency, parent_meta):
        name = dependency["Name"]
        all_versions = self.__trotter.get_versions(name)
        dependent_packet_full_names = self.__get_dependent_versions(parent_meta, name, all_versions)
        packets_to_install = self.__select_strategy.select_packets(dependent_packet_full_names)

        return packets_to_install[0]

    def __remove_packets(self, packet_full_names, docker):
        successful_remove_sequence = []
        first_removal_list = packet_full_names
        second_removal_list = []

        while first_removal_list: 
            for packet_full_name in first_removal_list:
                try:
                    docker.remove(str(packet_full_name))
                    if packet_full_name in second_removal_list:
                        second_removal_list.remove(packet_full_name)
                    if not packet_full_name in successful_remove_sequence:
                        successful_remove_sequence.append(packet_full_name)
                except DockerRemoveError, ex:
                    for dependent_packet in ex.dependent_packet_full_names:
                        if not dependent_packet in first_removal_list:
                            raise
                        if not dependent_packet in second_removal_list:
                            second_removal_list.append(dependent_packet)
                        if not packet_full_name in second_removal_list:
                            second_removal_list.append(packet_full_name)

            first_removal_list = second_removal_list
            second_removal_list = []

        return successful_remove_sequence

    def __install_packets(self, packet_full_names, docker):
        for packet_full_name in packet_full_names:
            self.__install(packet_full_name, docker)

    def __install(self, full_name_object, docker):
        if not self.__is_packet_installed(docker, str(full_name_object)):
            path = self.__locate_package(full_name_object)
            self.__install_uninstalled_dependencies(docker, path)
            docker.install(packet_path=path)

    def __is_packet_installed(self, docker, packet_fullname):
        return bool(docker.search(full_name=packet_fullname, status=Status.INSTALLED))

    def __locate_package(self, full_name_object):
        abs_path = ""
        full_name = str(full_name_object)
        if self.__cache.is_stored(full_name):
            abs_path = self.__cache.get_item(full_name)
        else:
            abs_path = self.__trotter.download(full_name_object.name, full_name_object.version)
            self.__cache.put_item(abs_path, full_name)

        return abs_path

    def __install_uninstalled_dependencies(self, docker, path):
         packet = FilePacket(path)
         meta = packet.meta

         for dependency in meta.depends:
            packet_fullname = self.__select_dependent_packet_full_name(dependency, meta)
            self.__install(packet_fullname, docker)

    def __get_dependent_versions(self, meta, name, versions):
        return [FullName(name, version) for version in versions if meta.is_in_depends(name, version)]

class GafferFactory:
    class __impl:
        def __init__(self, reader):
            self.__reader = reader
            self.__location_table_win = {"all_users": "ALLUSERSPROFILE", "user": "USERPROFILE"}
            self.__location_table_nix = {"user": "HOME"}
            self.__logger = logging.getLogger("GafferFactory")
            
        def get_gaffer(self, config_path = None):
            if not config_path:                                       
                config_path = os.path.join(sys.exec_prefix, "Scripts", "gaffer_config.yaml")
            if not os.path.exists(config_path):    
                message = "Not found file '%s' for docker configuration!" % config_abs_path   
                __logerror__(self.__logger,message)         
                raise GafferFactoryError(message)
            
            self.__logger.info("Configuration file '%s' will be used for gaffer" % config_path)
            options = self.__reader.read(config_path)
            docker_config_uri = None
            
            if "cache_folder" in options.keys():
                cache_folder = options["cache_folder"]
                self.__logger.debug("cache folder: %s" % cache_folder)
            if "cache_location" in options.keys():
                cache_location = options["cache_location"]
                self.__logger.debug("cache location: %s" % cache_location)
            if "docker_config" in options.keys():                
                docker_config_uri = options["docker_config_uri"]
                self.__logger.debug("docker_config_uri: %s" % docker_config_uri)
         
            if docker_config_uri:
                docker = DockerFactory().get_docker(docker_config_uri)                
            else:
                docker = DockerFactory().get_docker()
                
            if sys.platform == "win32":
                cache_location_path = os.environ[self.__location_table_win[cache_location]]                
            else:
                if cache_location == "all_users":
                    message = "Configuration option: %s for platform: %s is not supported" % (location ,sys.platform)
                    __logerror__(self.__logger,message)         
                    raise DockerFactoryError (message)                
                cache_location_path =  os.environ[self.__location_table_nix[cache_location]]                
             
            cache_path =  os.path.join(cache_location_path, cache_folder)
            cache = Cache(cache_path)   
            trotter = TrotterFactory().get_trotter(options)
            catalogue = Catalogue(trotter)
            select_strategy = LatestStrategy()            

            return Gaffer(cache, docker, trotter, catalogue, select_strategy)

    __instance = None

    def __init__(self, reader = ConfigReader()):
        if GafferFactory.__instance is None:
            GafferFactory.__instance = GafferFactory.__impl(reader)

        self.__dict__['_GafferFactory__instance'] = GafferFactory.__instance

    def __getattr__(self, attr):
        return getattr(self.__instance, attr)

    def __setattr__(self, attr, value):
        return setattr(self.__instance, attr, value)